﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace CodeContrib.Caching
{
	public class InMemoryCache : ICache, IDisposable
	{
		private readonly Dictionary<string, CacheItem> _cacheItems;
		private readonly ReaderWriterLockSlim _cacheItemsLock;
		private readonly Dictionary<string, HashSet<CacheItem>> _dependentCacheItems = new Dictionary<string, HashSet<CacheItem>>();
		private CacheItem _lruChainHead;
		private CacheItem _lruChainTail;
		private readonly object _lruLock = new object();
		private readonly int _maxCacheItemsCount;

		internal Func<DateTime> CurrentDateTime { get; set; }

		public InMemoryCache() : this(Int32.MaxValue)
		{
		}

		public InMemoryCache(int maxCacheItemsCount)
		{
			_cacheItems = new Dictionary<string, CacheItem>();
			_cacheItemsLock = new ReaderWriterLockSlim();
			CurrentDateTime = () => DateTime.UtcNow;
			_maxCacheItemsCount = maxCacheItemsCount;
		}

		public void Add(string key, object value, IEnumerable<string> dependentSets, TimeSpan slidingExpiration, DateTime absoluteExpiration)
		{
			Guard.NotNullOrEmpty(key, "key");

			var newCacheItem = new CacheItem
				                   {
					                   AbsoluteExpiration = (slidingExpiration > TimeSpan.Zero) ? CurrentDateTime().Add(slidingExpiration) : absoluteExpiration,
					                   DependentSets = dependentSets,
					                   Key = key,
					                   SlidingExpiration = slidingExpiration,
					                   Value = value
				                   };

			_cacheItemsLock.EnterWriteLock();

			CacheItem existingCacheItem;

			if (_cacheItems.TryGetValue(key, out existingCacheItem))
			{
				Remove(existingCacheItem);
			}

			if (_cacheItems.Count >= _maxCacheItemsCount)
			{
				Remove(_lruChainTail);
			}

			_cacheItems.Add(key, newCacheItem);

			if (dependentSets != null)
			{
				foreach (var dependentSet in dependentSets)
				{
					HashSet<CacheItem> dependentCacheItems;

					if (!_dependentCacheItems.TryGetValue(dependentSet, out dependentCacheItems))
					{
						dependentCacheItems = new HashSet<CacheItem>();

						_dependentCacheItems[dependentSet] = dependentCacheItems;
					}

					dependentCacheItems.Add(newCacheItem);
				}
			}

			MoveToTopOfLruChain(newCacheItem);

			newCacheItem.LastAccessedAt = CurrentDateTime();

			_cacheItemsLock.ExitWriteLock();
		}

		public void Dispose()
		{
			_cacheItemsLock.Dispose();
		}

		public bool Find(string key, out object value)
		{
			CacheItem cacheItem;

			_cacheItemsLock.EnterReadLock();

			var success = _cacheItems.TryGetValue(key, out cacheItem);

			_cacheItemsLock.ExitReadLock();

			if (success)
			{
				if (CurrentDateTime()
				    >= cacheItem.AbsoluteExpiration)
				{
					success = false;

					RemoveExpiredCacheItems();
				}
			}

			if (!success)
			{
				value = null;

				return false;
			}

			MoveToTopOfLruChain(cacheItem);

			if (cacheItem.SlidingExpiration
			    > TimeSpan.Zero)
			{
				cacheItem.AbsoluteExpiration = CurrentDateTime().Add(cacheItem.SlidingExpiration);
			}

			value = cacheItem.Value;

			return true;
		}

		private void MoveToTopOfLruChain(CacheItem cacheItem)
		{
			lock (_lruLock)
			{
				if (cacheItem != _lruChainHead)
				{
					if (cacheItem == _lruChainTail)
					{
						_lruChainTail = _lruChainTail.Previous;
					}

					if (cacheItem.Previous != null)
					{
						cacheItem.Previous.Next = cacheItem.Next;
					}

					if (cacheItem.Next != null)
					{
						cacheItem.Next.Previous = cacheItem.Previous;
					}

					if (_lruChainHead != null)
					{
						_lruChainHead.Previous = cacheItem;
					}

					cacheItem.Next = _lruChainHead;
					cacheItem.Previous = null;

					_lruChainHead = cacheItem;

					if (_lruChainTail == null)
					{
						_lruChainTail = cacheItem;
					}
				}
			}
		}

		public void Remove(string key)
		{
			_cacheItemsLock.EnterWriteLock();

			CacheItem cacheItem;

			if (_cacheItems.TryGetValue(key, out cacheItem))
			{
				Remove(cacheItem);
			}

			_cacheItemsLock.ExitWriteLock();
		}

		public void Remove(IEnumerable<string> keys)
		{
			Guard.NotNull(keys, "keys");

			_cacheItemsLock.EnterWriteLock();

			foreach (var key in keys)
			{
				HashSet<CacheItem> dependentCacheItems;

				if (_dependentCacheItems.TryGetValue(key, out dependentCacheItems))
				{
					foreach (var cacheItem in dependentCacheItems.ToArray())
					{
						Remove(cacheItem);
					}
				}
			}

			_cacheItemsLock.ExitWriteLock();
		}

		private void Remove(CacheItem cacheItem)
		{
			RemoveFromLruChain(cacheItem);

			_cacheItems.Remove(cacheItem.Key);

			if (cacheItem.DependentSets != null)
			{
				foreach (var dependentSet in cacheItem.DependentSets)
				{
					_dependentCacheItems[dependentSet].Remove(cacheItem);
				}
			}
		}

		private void RemoveExpiredCacheItems()
		{
			_cacheItemsLock.EnterWriteLock();

			CacheItem nextCacheItem;

			for (var expiredCacheItem = _lruChainHead; expiredCacheItem != null; expiredCacheItem = nextCacheItem)
			{
				nextCacheItem = expiredCacheItem.Next;

				if (CurrentDateTime()
				    >= expiredCacheItem.AbsoluteExpiration)
				{
					Remove(expiredCacheItem);
				}
			}

			_cacheItemsLock.ExitWriteLock();
		}

		private void RemoveFromLruChain(CacheItem cacheItem)
		{
			lock (_lruLock)
			{
				if (cacheItem == _lruChainHead)
				{
					_lruChainHead = _lruChainHead.Next;
				}

				if (cacheItem.Previous != null)
				{
					cacheItem.Previous.Next = cacheItem.Next;
				}
				else
				{
					_lruChainHead = cacheItem.Next;
				}

				if (cacheItem.Next != null)
				{
					cacheItem.Next.Previous = cacheItem.Previous;
				}
				else
				{
					_lruChainTail = cacheItem.Previous;
				}
			}
		}
	}
}